﻿using System;
using System.Windows;
using System.Collections.Generic;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Media;
using System.Windows.Media.Media3D;
using Bling.DSL;
using Bling.WPF;
using Bling.Arrays;
using Bling.Shapes;
using Bling.Animation;
using Bling.Util;
using Bling.Physics;
using Bling.D3D;
using Bling.Lighting;


namespace Bling.PhysicsTutorial {
  public class CurtainBlock : ImmediatePhysicsBlock<CurtainBlock>, Position3DBody.Is<CurtainBlock>, MeshBody.Is<CurtainBlock> {
    public CurtainBlock(World World, int ColumnCount, int RowCount)
      : base(World, ColumnCount * RowCount) {
      new Position3DBody.Impl<CurtainBlock>(this);
      new MeshBody.Impl<CurtainBlock>(this, ColumnCount, RowCount);

      PreserveProperty = this.NewBodyProperty<bool>("Preserve");
      Preserve = (body) => body.GetProperty(PreserveProperty);
    }

    public static PhysicsProperty<bool> PreserveProperty;
    public Func<CurtainBlock.Body, BoolBl> Preserve;

    protected override void Init() {
      base.Init();

      this.ForAll(body => {

        //Preserve(body).Init = false; 

        body.Position3D().VerletStep(.025);
        Point3DBl p = new PointBl(0, 0);
        var useBody = body.CursorPosition3D();
        // simplify the expression of springs.
        p += useBody.Spring(+1, +0);
        p += useBody.Spring(-1, +0);
        p += useBody.Spring(+0, +1);
        p += useBody.Spring(+0, -1);
        p += useBody.Spring(-1, -1);
        p += useBody.Spring(+1, +1);

        p /= 6d;
        //p = Preserve(body).Condition(body.Position3D().Old, p);

        //body.Position3D().Bind = p;
        body.Position3D().Relax[1] = p;


        body.Position3D().ClampWithRestitution(this.MeshBodyE().LeftTop, this.MeshBodyE().LeftTop + this.MeshBodyE().Size);
      });
    }
  }

  public class Curtain : PhysicsCanvas {
    public Curtain() : base("Curtain2", 30) { }
    protected override Action Init(CanvasBl MainCanvas, World World) {
      var Flowers = Properties.Resources.flowers;
      Background = Brushes.LightBlue;
      int ColumnCount = 15;
      int RowCount = 10;
      // There will be one row that is not modelled in block.
      var block = new CurtainBlock(World, ColumnCount, RowCount - 1);
      var block2 = new CurtainBlock(World, ColumnCount, RowCount - 1);

      // Control when 3D is used.
      var DoThumbs = new CheckBoxBl(MainCanvas) {
        LeftTop = new PointBl(0, 0),
        ZIndex = 100, Content = "Thumbs", IsChecked = false,
      };
      var Do3D = new CheckBoxBl(MainCanvas) {
        LeftTop = DoThumbs.RightTop,
        ZIndex = 100, Content = "3D", IsChecked = true,
        //IsEnabled = DoThumbs.IsChecked,
      };
      var DoRight = new CheckBoxBl(MainCanvas) {
        LeftTop = Do3D.RightTop,
        ZIndex = 100, Content = "Right", IsChecked = false,
      };
      var ConsumeWidth = new LabelSliderBl(MainCanvas) {
        LeftTop = DoThumbs.LeftBottom, ZIndex = 100,
        Minimum = 0.1, Maximum = 1, Value = 0.5, LabelName = "Consume",
      };
      var Close = new LabelSliderBl(MainCanvas) {
        LeftTop = ConsumeWidth.LeftBottom, ZIndex = 100,
        Minimum = 0.1, Maximum = 1, Value = 1, LabelName = "Close",
      };

      DoubleBl ratio = (MainCanvas.Size.X * ConsumeWidth / MainCanvas.Size.Y.Max(1));
      PointBl MeshSize = new PointBl(ratio, 1d);
      {
        PointBl UseLeftTop = new PointBl(0, 1d / ((double)RowCount));
        PointBl UseMeshSize = (MeshSize - UseLeftTop)*new PointBl(Close, 1);
        // Offset block up by one row since there is one row it does not contain.
        block.MeshBodyE().LeftTop = UseLeftTop
          + new PointBl(DoRight.IsChecked.Condition((MeshSize - UseLeftTop).X * (1d - Close.Value), 0d), 0);
        block.MeshBodyE().Size = UseMeshSize;

        block2.MeshBodyE().LeftTop = UseLeftTop
          + new PointBl((!DoRight.IsChecked).Condition((MeshSize - UseLeftTop).X * (1d - Close.Value), 0d), 0);
        block2.MeshBodyE().Size = UseMeshSize;
      }
      // DEBUG: provide thumbs for block of 3D positions.
      var thumbs = new PlainBlock<Thumb, ThumbBl>(World, block.Count, (idx) => new ThumbBl(MainCanvas) {
        Background = idx.SelectColor(), CanDrag = true, ZIndex = 1, Visibility = DoThumbs.IsChecked,
      });

      var thumbs2 = new PlainBlock<Thumb, ThumbBl>(World, block.Count, (idx) => new ThumbBl(MainCanvas) {
        Background = idx.SelectColor(), CanDrag = true, ZIndex = 1, Visibility = DoThumbs.IsChecked,
      });

      // DEBUG: this just provides us with a slot for the zindex, we don't show it any where.
      var sliders = new PlainBlock<Slider, SliderBl>(World, block.Count, (idx) => new SliderBl() {
        Background = idx.SelectColor(), Minimum = 0, Maximum = 1.0, Value = (idx % ColumnCount % 4 > 0) ? 0.05 : 0.0,
      });
      // for 2D debug.
      for (int i = 0; i < block.ColumnCount() - 1; i++) {
        DoubleBl di = ((double)i);
        di = di / ((double)(block.ColumnCount() - 2));
        for (int j = 0; j < block.RowCount() - 1; j++) {
          DoubleBl dj = j;
          dj = dj / ((double)(block.RowCount() - 2));
          var shape = new PathShapeBl(MainCanvas) {
            Start = thumbs[block.Get(i, j).Index].At, ZIndex = 0,
            Fill = (di.Lerp(Colors.Red, Colors.Blue) * .5 +
                    dj.Lerp(Colors.Green, Colors.Purple) * .5),
            Visibility = !Do3D.IsChecked,
            Segments = {
              AddMany = new PathSegmentBl[] {
              thumbs[block.Get(i + 1, j + 0).Index].At.CenterPosition,
              thumbs[block.Get(i + 1, j + 1).Index].At.CenterPosition,
              thumbs[block.Get(i + 0, j + 1).Index].At.CenterPosition,
              thumbs[block.Get(i + 0, j + 0).Index].At.CenterPosition,
            },
            },
          };
        }
      }
      // displace thumbs/3D according to whether or not this is a right curtain.
      // DEBUG: setup relationship with thumbs.
      //var slider2 = new SliderBl(MainCanvas) {
      //  LeftTop = Close.LeftBottom, Minimum = 0.0, Maximum = 1.0, Value = 0.0, Width = MainCanvas.Width, ZIndex = 100
      //};

      PointBl LeftTopOffset2 = new PointBl((!DoRight.IsChecked).Condition(MainCanvas.Width * (1d - ConsumeWidth.Value), 0), 0);
      block2.ForAll(body => {
        var thumb = thumbs2[body.Index].At;
        var slider = sliders[body.Index].At;
        Point3DBl p = body.Position3D();
        ((ObjectBl)("X: ".Bl() + p.RoundN(100d))).Link.To = thumb.ToolTip;
        var sz = MainCanvas.Size * new PointBl(ConsumeWidth, 1);
        p = p * new Point3DBl(sz / MeshSize, 1d);
        p = p + LeftTopOffset2;

        p.Link[!thumb.IsDragging] = new Point3DBl(thumb.CenterPosition, slider.Value);
      });
      
      PointBl LeftTopOffset = new PointBl(DoRight.IsChecked.Condition(MainCanvas.Width * (1d - ConsumeWidth.Value), 0), 0);
      block.ForAll(body => {
        var thumb = thumbs[body.Index].At;
        var slider = sliders[body.Index].At;
        Point3DBl p = body.Position3D();
        ((ObjectBl)("X: ".Bl() + p.RoundN(100d))).Link.To = thumb.ToolTip;
        var sz = MainCanvas.Size * new PointBl(ConsumeWidth, 1);
        p = p * new Point3DBl(sz / MeshSize, 1d);
        p = p + LeftTopOffset;

        p.Link[!thumb.IsDragging] = new Point3DBl(thumb.CenterPosition, slider.Value);
      });
      // Create a new grid mesh. Will manage columns/rows itself.
      var clothMesh = new GridMesh(ColumnCount, RowCount);
      var clothMesh2 = new GridMesh(ColumnCount, RowCount);

      var cloth = new CanvasBl() {
        Background = new ImageBrushBl() {
          ImageSource = Flowers,
          TileMode = TileMode.Tile,
          Stretch = Stretch.Fill,
        },
        Size = new PointBl(MainCanvas.Width * ConsumeWidth, MainCanvas.Height),
      };
      var cloth2 = new CanvasBl() {
        Background = new ImageBrushBl() {
          ImageSource = Flowers,
          TileMode = TileMode.Tile,
          Stretch = Stretch.Fill,
        },
        Size = new PointBl(MainCanvas.Width * ConsumeWidth, MainCanvas.Height),
      };      
      
      // all the standard 3D stuff.
      var vp3D = new Viewport3DBl(MainCanvas) {
        LeftTop = new PointBl(0, 0), ZIndex = 0,
        Size = new PointBl(MainCanvas.Width * ConsumeWidth, MainCanvas.Height),
        Visibility = Do3D.IsChecked,
        FlatPerspective = MeshSize,
        Children = {
          AddOne = new Viewport2DVisual3DBl() {
            Material = new DiffuseMaterialBl() {
              IsVisualHostMaterial = true, // XXX: should freeze.
            },
            CachingHint = CachingHint.Cache,
            CacheInvalidationThreshold = { Minimum = 0.001, Maximum = 1000, },
            Visual = cloth,
            Geometry = clothMesh,
          },
        },
      };
      var vp3D2 = new Viewport3DBl(MainCanvas) {
        LeftTop = new PointBl(MainCanvas.Width *0.5, 0), ZIndex = 0,
        Size = new PointBl(MainCanvas.Width * ConsumeWidth, MainCanvas.Height),
        Visibility = Do3D.IsChecked,
        FlatPerspective = MeshSize,
        Children = {
          AddOne = new Viewport2DVisual3DBl() {
            Material = new DiffuseMaterialBl() {
              IsVisualHostMaterial = true, // XXX: should freeze.
            },
            CachingHint = CachingHint.Cache,
            CacheInvalidationThreshold = { Minimum = 0.001, Maximum = 1000, },
            Visual = cloth2,
            Geometry = clothMesh2,
          },
        },
      };
      //adjust the light to make the two curtains looks like be lighten by the same light.
      vp3D.Children.AddOne = new ModelVisual3DBl() {
        Content = new SpotLightBl() {
          Color = Colors.White,
          Position = new Point3DBl(MeshSize.X, MeshSize.Y * 0.5, 1.0),
          Direction = new Point3DBl(0, 0, -1),//(/*1.2*/ sliderA.Value, /*0.1*/ sliderB.Value, /*-1.1*/ sliderC.Value),
          InnerConeAngle = 70d.ToDegrees(),
          OuterConeAngle = 100d.ToDegrees(),
        },
      };
      vp3D2.Children.AddOne = new ModelVisual3DBl() {
        Content = new SpotLightBl() {
          Color = Colors.White,
          Position = new Point3DBl(0, MeshSize.Y * 0.5, 1.0),
          Direction = new Point3DBl(0, 0, -1),//(/*1.2*/ sliderA.Value, /*0.1*/ sliderB.Value, /*-1.1*/ sliderC.Value),
          InnerConeAngle = 70d.ToDegrees(),
          OuterConeAngle = 100d.ToDegrees(),
        },
      };
      var box = new TextBoxBl(MainCanvas) {
        LeftBottom = new PointBl(0, MainCanvas.Height - 50d),
        Width = MainCanvas.Width * 0.5, ZIndex = 100,
        Font = { Size = 40, Weight = FontWeights.SemiBold, /*Family = "Arial".Convert<FontFamily>(),*/ },
        Background = Brushes.Transparent, Foreground = (!Do3D.IsChecked).Condition(Brushes.Black, Brushes.Transparent),
      };
      var box2 = new TextBlockBl(cloth) {
        Text = box.Text, Font = box.Font,
        LeftTop = box.LeftTop, Size = box.Size,
        Foreground = Brushes.Black,
      };


      return () => {
        Action<int> doSomething = Point3DBl.Assign<int>(i => block[i].Position3D(), i => block[i].Position3D() + new Point3DBl(.0, .0, -0.2));
        Action<int> doSomething2 = Point3DBl.Assign<int>(i => block2[i].Position3D(), i => block2[i].Position3D() + new Point3DBl(.0, .0, -0.2));
        Action<int> doSomethingElse = Point3DBl.Assign<int>(i => block[i].Position3D(), i => block[i].Position3D() +
          new Point3DBl(-240, 0, -0.1)
          );
        //Action<int> togglePreserve = BoolBl.Assign<int>(i => block.Preserve(block[i]), i => !block.Preserve(block[i]));

        Random rr = new Random();
        var button = new ButtonBl(MainCanvas) {
          RightBottom = MainCanvas.Size,
          Content = "Push", Click = () => {
            if (rr.Next(2) > 0)
              doSomething(rr.Next(0, block.Count));
            else
              doSomething2(rr.Next(0, block2.Count));
          },
          ZIndex = 100,
        };
        //var button2 = new ButtonBl(MainCanvas) {
        //  LeftBottom = button.RightBottom,
        //  Content = "Push a hole", Click = () => {
        //    for (int i = 4; i < RowCount - 3; i++) {
        //      doSomethingElse(i * ColumnCount - 1);
        //      if (i == 5 || i == 6) {
        //        doSomethingElse(i * ColumnCount - 1);
        //      }
        //      togglePreserve(i * ColumnCount - 1);
        //    }
        //  },
        //  ZIndex = 100,
        //};

        var cbox = box.CurrentValue;
        cbox.TextChanged += (x, y) => {
          if (y.Changes.Count == 1) {
            foreach (var change in y.Changes) {
              if ((change.AddedLength == 1 && change.RemovedLength == 0)
              || (change.AddedLength == 0 && change.RemovedLength == 1)) {
                var xPos = cbox.GetRectFromCharacterIndex(cbox.Text.Length).Left;

                var pos = new PointBl(xPos, MainCanvas.Height);
                pos = pos / MainCanvas.Size;
                pos = pos * MeshSize;
                //int row = (int)(pos.Y / y_step).CurrentValue;
                //row = (row >= RowsN) ? RowsN - 1 : row;
                var x_step = MeshSize.X / (ColumnCount - 1);
                for (int row = 0; row < RowCount - 1; row++) {
                  int col = (int)(pos.X * 2.0 / x_step).CurrentValue;
                  col = Math.Min(col, ColumnCount - 1);
                  doSomething(row * ColumnCount + col);
                }
              }
            }
          }
        };

        MainCanvas.OnMouseDown = (x, y) => {
          var pos = MainCanvas.MousePosition;
          var x_step = MeshSize.X / (ColumnCount - 1);
          var y_step = MeshSize.Y / (RowCount - 1);
          if ((pos.X < MainCanvas.Width / 2.0).CurrentValue) {
            pos = pos / MainCanvas.Size;
            pos = pos * MeshSize;
            int row = (int)(pos.Y / y_step).CurrentValue;
            row = (row >= RowCount - 1) ? RowCount - 2 : row;
            int col = (int)(pos.X * 2 / Close / x_step).CurrentValue;
            col = (col >= ColumnCount) ? ColumnCount - 1 : col;
            doSomething(row * ColumnCount + col);
            return true;
          } else if ((pos.X > MainCanvas.Width / 2.0).CurrentValue){
            pos = (pos- new PointBl(MainCanvas.Width / 2.0 * (2.0 - Close.Value) , 0)) / MainCanvas.Size;
            pos = pos * MeshSize;
            int row = (int)(pos.Y / y_step).CurrentValue;
            row = (row >= RowCount - 1) ? RowCount - 2 : row;
            int col = ((int)(pos.X * 2 / Close / x_step).CurrentValue);
            col = (col >= ColumnCount) ? ColumnCount - 1 : col;
            doSomething2(row * ColumnCount + col);
            return true;
          }
          return false;
        };

        // convert to 2D array by Columns.
        var array = block.Position3DArray().Horizontal2D(ColumnCount);
        // flip Y because Y grows up in 3D instead of down.
        array = array.Map((i,j,p) => new Point3DBl(p.Bl().X, 1 - p.Bl().Y, p.Bl().Z));
        //array[0,1] = new Point3DBl(1,1,1);
        // likewise, reverse the rows because Y grows up. 
        array = array.ReverseRows;
        // splice in a last row to pin the curtain to the top
        var lastRow = new Array2D<Point3D>(array.ColumnCount, 1, (i, j) => {
          return new Point3DBl(i * block.MeshBodyE().CellSize.X + block.MeshBodyE().LeftTop.X, 1d, 0);
        });
        // since block has only RowCount - 1 rows, the resulting array has RowCount rows.
        array = array.SpliceRows(array.RowCount, lastRow);

       
        var closeValue = 1 - Close.Value;
        var lv = 5;
        var array3 = new Array2D<Point3D>(array.ColumnCount, array.RowCount, (i, j) => {
          var j0 = (j == ColumnCount - 1).Condition(j-1, j);
          var cx = (j0 > 6).Condition(((j0 * j0 * 0.025.Bl())).Clamp(0,1d/Close.Value), 1.0);
          return new Point3DBl(array[i, j].Bl().X, array[i, j].Bl().Y + (j == 0 & i % 4 > 1).Condition(0.02, 0), array[i, j].Bl().Z + (i % 4 > 1).Condition(-0.1 * closeValue, 0.0));
        });
        // Refresh in UI on each time step.
        World.UpdateInUI.Add(clothMesh.Refresh(array3));

        // convert to 2D array by Columns.
        var array2 = block2.Position3DArray().Horizontal2D(ColumnCount);
        // flip Y because Y grows up in 3D instead of down.
        array2 = array2.Map((i,j,p) => new Point3DBl(p.Bl().X, 1 - p.Bl().Y, p.Bl().Z));
        //array[0,1] = new Point3DBl(1,1,1);
        // likewise, reverse the rows because Y grows up. 
        array2 = array2.ReverseRows;
        // splice in a last row to pin the curtain to the top
        lastRow = new Array2D<Point3D>(array.ColumnCount, 1, (i, j) => {
          return new Point3DBl(i * block2.MeshBodyE().CellSize.X + block2.MeshBodyE().LeftTop.X, 1d, 0);
        });
        // since block has only RowCount - 1 rows, the resulting array has RowCount rows.
        array2 = array2.SpliceRows(array2.RowCount, lastRow);

        array3 = new Array2D<Point3D>(array2.ColumnCount, array2.RowCount, (i, j) => {
          var j0 = (j == ColumnCount - 1).Condition(j - 1, j);
          var cx = (j0 > 6).Condition(((j0 * j0 * 0.025.Bl())).Clamp(0, 1d / Close.Value), 1.0);
          return new Point3DBl(array2[i, j].Bl().X, array2[i, j].Bl().Y + (j == 0 & i % 4 > 1).Condition(0.02, 0), array2[i, j].Bl().Z + (i % 4 > 1).Condition(-0.1 * (1 - Close.Value), 0.0));
        });
        World.UpdateInUI.Add(clothMesh2.Refresh(array3));
      };
    }
  }


}
